#!/usr/bin/env bash

is_mac() {
    if [ "$(uname -s)" != "Darwin" ]; then
        echo "You need a mac to build this package"
        exit 1
    fi
}

CURDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VERSION="$(git describe HEAD)"
# Only append date if we're not on a whole version, like v2018.11
if echo "${VERSION}" | grep -q -- "-"; then
    VERSION="${VERSION}_$(git describe HEAD | xargs git show -s --format=format:"%cd" --date=short)"
fi

# Default Android build to arm.
ANDROID_ARCH="${ANDROID_ARCH:-arm}"

# Default to Android 4.0+; required for NDK 15 but with a custom NDK the strict minimum is 9.
NDKABI="${NDKABI:-14}"
export NDKABI

# Default android flavor
ANDROID_FLAVOR="${ANDROID_FLAVOR:-rocks}"
export ANDROID_FLAVOR

function assert_ret_zero() {
    if [ "${1}" -ne 0 ]; then
        if [ -n "${2}" ]; then
            echo "${2}"
        fi
        exit 1
    fi
}

function check_submodules() {
    if git submodule status | grep -qE '^\-'; then
        kodev-fetch-thirdparty
    fi
}

# Takes two arguments:
# $1 arguments to pass to pgrep
# $2 process name to pgrep for
function gnuplot_wrapper() {
    # inspired by https://gist.github.com/nicolasazrak/32d68ed6c845a095f75f037ecc2f0436
    trap capture_ctrl_c INT
    TEMP_DIR="$(mktemp --directory /tmp/tmp.koreaderXXX)"
    LOG="${TEMP_DIR}/memory.log"
    SCRIPT_PNG="${TEMP_DIR}/script_png.p"
    SCRIPT_SHOW="${TEMP_DIR}/script_show.p"
    IMAGE_PNG="${TEMP_DIR}/graph.png"

    echo "Memory plot output to ${TEMP_DIR}"

    cat >"${SCRIPT_PNG}" <<EOL
set term pngcairo size 1600,1200
set output "${IMAGE_PNG}"
set ylabel "RSS"
set y2label "VSZ"
set ytics nomirror
set y2tics nomirror in
set yrange [0:*]
set y2range [0:*]
plot "${LOG}" using 3 with lines axes x1y1 title "RSS", "${LOG}" using 2 with lines axes x1y2 title "VSZ"
EOL

    cat >"${SCRIPT_SHOW}" <<EOL
set term wxt noraise
set ylabel "RSS"
set y2label "VSZ"
set ytics nomirror
set y2tics nomirror in
set yrange [0:*]
set y2range [0:*]
plot "${LOG}" using 3 with lines axes x1y1 title "RSS", "${LOG}" using 2 with lines axes x1y2 title "VSZ"
pause 1
reread
EOL

    function capture_ctrl_c() {
        kill "${LOOP_PID}"
        kill "${GNUPLOT_PID}"
        gnuplot "${SCRIPT_PNG}"
        exit
    }

    # initialize at 0 so gnuplot has something to show
    echo "0 0 0" >"${LOG}"
    while true; do
        # shellcheck disable=SC2086
        ps -p "$(pgrep --delimiter ' ' $1 "$2")" -o pid=,vsz=,rss= >>"${LOG}"
        sleep 1
    done &
    LOOP_PID=$!
    gnuplot "${SCRIPT_SHOW}" &
    GNUPLOT_PID=$!
}

function setup_env() {
    SETUP_ENV_GREP_COMMAND="grep -z -v debug"
    if [ -n "${KODEBUG}" ]; then
        SETUP_ENV_GREP_COMMAND="grep -z debug"
        # for android adb install
        KODEBUG_SUFFIX=-debug
    fi
    local files=()
    while IFS= read -r -d $'\0'; do
        files+=("${REPLY}")
    done < <(find . -maxdepth 1 -name 'koreader-emulator-*' -print0 | ${SETUP_ENV_GREP_COMMAND})
    test ${#files[@]} -gt 0
    assert_ret_zero $? "Emulator not found. Please build it first."
    local idx=0
    # Warn if multiple matches were found
    if [ ${#files[@]} -gt 1 ]; then
        echo "Multiple emulator builds found:"
        local ts=()
        # Store list of ts at the same index
        for i in "${!files[@]}"; do
            local file="${files[${i}]}/koreader"
            if [ -d "${file}" ]; then
                echo "${file} (last modified on $(stat -c %y "${file}"))"
                ts[${i}]="$(stat -c %Y "${file}")"
            fi
        done
        # Sort the list of ts
        local sorted_ts=()
        IFS=$'\n' read -d '' -r -a sorted_ts < <(printf '%s\n' "${ts[@]}" | sort -r)
        # Find the id of the most recent ts (spoiler: it's going to be the one currently targeted by this invocation of kodev)
        for i in "${!ts[@]}"; do
            if [ "${ts[${i}]}" == "${sorted_ts[0]}" ]; then
                idx="${i}"
                break
            fi
        done
        # Recap
        echo "Picking the most recent one: ${files[${idx}]}/koreader"
    fi
    EMU_DIR="${files[${idx}]}/koreader"
    export EMU_DIR
    KO_LD_LIBRARY_PATH="$(realpath "${EMU_DIR}")/libs:${LD_LIBRARY_PATH}"
    # Don't export it to avoid messing with tools (e.g., debuggers).
    # We'll pass it to LuaJIT via env
}

function kodev-fetch-thirdparty() {
    make fetchthirdparty
}

SUPPORTED_TARGETS="
    kindle          Compatible with all Kindle models >= Kindle4
    kindlepw2       With compiler optimizations for Kindle models >= Paperwhite 2
    kindle-legacy   Needed only for Kindle2/3/DXG
    kobo
    cervantes
    remarkable
    sony-prstux
    android
    pocketbook
    ubuntu-touch
    appimage
    debian          Debian package for current arch
    debian-armel    Debian package for generic armel devices
    debian-armhf    Debian package for generic armhf devices
    debian-arm64    Debian package for generic 64 bits arm devices
    macos           MacOS app bundle. You need a mac to build this package
    emu             (*default) If no TARGET is given, assume emulator
    win32
"

function kodev-build() {
    BUILD_HELP_MSG="
usage: build <OPTIONS> <TARGET>

OPTIONS:

    -v, --verbose   make the buildsystem more verbose
    -d, --debug     include debugging symbols (default for emulator)
    -n, --no-debug  no debugging symbols (default for target devices)

TARGET:
${SUPPORTED_TARGETS}"

    declare opt
    declare -r E_OPTERR=85
    declare -r short_opts="vhnd"
    declare -r long_opts="verbose,help,no-debug,debug"

    if ! opt=$(getopt -o "${short_opts}" --long "${long_opts}" --name "kodev" -- "${@}"); then
        echo "${BUILD_HELP_MSG}"
        exit ${E_OPTERR}
    fi

    eval set -- "${opt}"

    while true; do
        PARAM="${1}"
        # Support using an = assignment with short options (e.g., -f=blah instead of -f blah).
        VALUE="${2/#=/}"
        case "${PARAM}" in
            -v | --verbose)
                export VERBOSE=1
                ;;
            -h | --help)
                echo "${BUILD_HELP_MSG}"
                exit 0
                ;;
            -n | --no-debug)
                export KODEBUG=
                KODEBUG_NO_DEFAULT=1
                ;;
            -d | --debug)
                export KODEBUG=1
                KODEBUG_NO_DEFAULT=1
                ;;
            --)
                break
                ;;
            *)
                echo "ERROR: unknown option \"${PARAM}\""
                echo "${BUILD_HELP_MSG}"
                exit 8
                ;;
        esac
        shift
    done
    shift

    check_submodules
    case "${1}" in
        cervantes)
            make TARGET=cervantes
            assert_ret_zero $?
            ;;
        kindle)
            make TARGET=kindle
            assert_ret_zero $?
            ;;
        kindlepw2)
            make TARGET=kindlepw2
            assert_ret_zero $?
            ;;
        kobo)
            make TARGET=kobo
            assert_ret_zero $?
            ;;
        remarkable)
            make TARGET=remarkable
            assert_ret_zero $?
            ;;
        sony-prstux)
            make TARGET=sony-prstux
            assert_ret_zero $?
            ;;
        kindle-legacy)
            make TARGET=kindle-legacy
            assert_ret_zero $?
            ;;
        android)
            if [ -z "${NDK}" ]; then
                if [ -n "${ANDROID_NDK_HOME}" ]; then
                    # some distributions use `ANDROID_NDK` instead, fall back to it
                    export NDK="${ANDROID_NDK_HOME}"
                else
                    export NDK="${CURDIR}/base/toolchain/android-ndk-r15c"
                fi
            fi
            [ -e "${CURDIR}/base/toolchain/android-toolchain-${ANDROID_ARCH}/bin/" ] || {
                { [ -e "${NDK}" ] || make -C "${CURDIR}/base/toolchain" android-ndk; }
                assert_ret_zero $?
                make android-toolchain
                assert_ret_zero $?
            }
            echo "Using NDK: ${NDK}..."
            make TARGET=android
            assert_ret_zero $?
            ;;
        pocketbook)
            make TARGET=pocketbook
            assert_ret_zero $?
            ;;
        ubuntu-touch)
            make TARGET=ubuntu-touch
            assert_ret_zero $?
            ;;
        appimage)
            make TARGET=appimage
            assert_ret_zero $?
            ;;
        debian)
            make TARGET=debian
            assert_ret_zero $?
            ;;
        debian-armel)
            make TARGET=debian-armel
            assert_ret_zero $?
            ;;
        debian-armhf)
            make TARGET=debian-armhf
            assert_ret_zero $?
            ;;
        debian-arm64)
            make TARGET=debian-arm64
            assert_ret_zero $?
            ;;
        macos)
            is_mac
            make TARGET=macos
            assert_ret_zero $?
            ;;
        win32)
            make TARGET=win32
            assert_ret_zero $?
            ;;
        *)
            if [ -z "${KODEBUG_NO_DEFAULT}" ]; then # no explicit --debug / --no-debug
                # builds a debug build by default, like kodev-run
                export KODEBUG=1
            fi
            make
            assert_ret_zero $? "Failed to build emulator! Try run with -v for more information."
            setup_env
            ;;
    esac
}

function kodev-clean() {
    CLEAN_HELP_MSG="
usage: clean <TARGET>

TARGET:
${SUPPORTED_TARGETS}"

    declare opt
    declare -r E_OPTERR=85
    declare -r short_opts="nd"
    declare -r long_opts="no-debug,debug"

    if ! opt=$(getopt -o "${short_opts}" --long "${long_opts}" --name "kodev" -- "${@}"); then
        echo "${CLEAN_HELP_MSG}"
        exit ${E_OPTERR}
    fi

    eval set -- "${opt}"

    while true; do
        PARAM="${1}"
        # Support using an = assignment with short options (e.g., -f=blah instead of -f blah).
        VALUE="${2/#=/}"
        case "${PARAM}" in
            -n | --no-debug)
                export KODEBUG=
                KODEBUG_NO_DEFAULT=1
                ;;
            -d | --debug)
                export KODEBUG=1
                KODEBUG_NO_DEFAULT=1
                ;;
            --)
                break
                ;;
            *)
                echo "ERROR: unknown option \"${PARAM}\""
                echo "${BUILD_HELP_MSG}"
                exit 8
                ;;
        esac
        shift
    done
    shift

    case "${1}" in
        -h | --help)
            echo "${CLEAN_HELP_MSG}"
            exit 0
            ;;
        cervantes)
            make TARGET=cervantes clean
            ;;
        kindle)
            make TARGET=kindle clean
            ;;
        kindlepw2)
            make TARGET=kindlepw2 clean
            ;;
        kobo)
            make TARGET=kobo clean
            ;;
        remarkable)
            make TARGET=remarkable clean
            ;;
        sony-prstux)
            make TARGET=sony-prstux clean
            ;;
        kindle-legacy)
            make TARGET=kindle-legacy clean
            ;;
        android)
            make TARGET=android clean
            rm -f ./*.apk
            ;;
        pocketbook)
            make TARGET=pocketbook clean
            ;;
        ubuntu-touch)
            make TARGET=ubuntu-touch clean
            ;;
        appimage)
            make TARGET=appimage clean
            ;;
        debian)
            make TARGET=debian clean
            ;;
        debian-armel)
            make TARGET=debian-armel clean
            ;;
        debian-armhf)
            make TARGET=debian-armhf clean
            ;;
        debian-arm64)
            make TARGET=debian-arm64 clean
            ;;
        macos)
            is_mac
            make TARGET=macos clean
            ;;
        win32)
            make TARGET=win32 clean
            ;;
        *)
            if [ -z "${KODEBUG_NO_DEFAULT}" ]; then
                # No explicit --debug / --no-debug
                # Builds a debug build by default, like kodev-run
                export KODEBUG=1
            fi

            make clean
            ;;
    esac
}

function kodev-release() {
    # SUPPORTED_RELEASE_TARGETS=$(echo ${SUPPORTED_TARGETS} | sed 's/win32//')
    SUPPORTED_RELEASE_TARGETS="${SUPPORTED_TARGETS/emu*/""}"
    RELEASE_HELP_MSG="
usage: release <OPTIONS> <TARGET>

OPTIONS:

    -d, --debug               debug-enabled release (for remote gdb)
    -i, --ignore-translation  do not fetch translation for release
    -v, --verbose             make the buildsystem more verbose

TARGET:
${SUPPORTED_RELEASE_TARGETS}"
    [ $# -lt 1 ] && {
        echo "${RELEASE_HELP_MSG}"
        exit 1
    }

    # Defaults
    ignore_translation=0

    declare opt
    declare -r E_OPTERR=85
    declare -r short_opts="divh"
    declare -r long_opts="debug,ignore-translation,verbose,help"

    if ! opt=$(getopt -o "${short_opts}" --long "${long_opts}" --name "kodev" -- "${@}"); then
        echo "${RELEASE_HELP_MSG}"
        exit ${E_OPTERR}
    fi

    eval set -- "${opt}"

    while true; do
        PARAM="${1}"
        # Support using an = assignment with short options (e.g., -f=blah instead of -f blah).
        VALUE="${2/#=/}"
        case "${PARAM}" in
            -d | --debug)
                export KODEBUG=1
                ;;
            -i | --ignore-translation)
                ignore_translation=1
                ;;
            -v | --verbose)
                export VERBOSE=1
                ;;
            -h | --help)
                echo "${RELEASE_HELP_MSG}"
                exit 0
                ;;
            --)
                break
                ;;
            *)
                echo "ERROR: unknown option \"${PARAM}\""
                echo "${RELEASE_HELP_MSG}"
                exit 1
                ;;
        esac
        shift
    done
    shift

    check_submodules
    if [ "${ignore_translation}" -eq 0 ]; then
        make po || {
            echo "ERROR: failed to fetch translation."
            echo "Tip: Use --ignore-translation OPTION if you want to build a release without translation."
            exit 1
        }
    fi

    case "${1}" in
        kindle)
            kodev-build kindle
            make TARGET=kindle update
            ;;
        kindlepw2)
            kodev-build kindlepw2
            make TARGET=kindlepw2 update
            ;;
        kobo)
            kodev-build kobo
            make TARGET=kobo update
            ;;
        remarkable)
            kodev-build remarkable
            make TARGET=remarkable update
            ;;
        sony-prstux)
            kodev-build sony-prstux
            make TARGET=sony-prstux update
            ;;
        cervantes)
            kodev-build cervantes
            make TARGET=cervantes update
            ;;
        kindle-legacy)
            kodev-build kindle-legacy
            make TARGET=kindle-legacy update
            ;;
        android)
            kodev-build android
            export PATH=${PATH}:${CURDIR}/base/toolchain/android-sdk-linux/tools
            command -v android &>/dev/null || {
                make -C "${CURDIR}/base/toolchain" android-sdk
            }
            ANDROID_HOME=$(dirname "$(dirname "$(command -v android)")")
            export ANDROID_HOME
            export PATH="${PATH}:${NDK}"
            make TARGET=android update
            ;;
        pocketbook)
            kodev-build pocketbook
            make TARGET=pocketbook update
            ;;
        ubuntu-touch)
            kodev-build ubuntu-touch
            make TARGET=ubuntu-touch update
            ;;
        appimage)
            kodev-build appimage
            make TARGET=appimage update
            ;;
        debian)
            kodev-build debian
            make TARGET=debian update
            ;;
        debian-armel)
            kodev-build debian-armel
            make TARGET=debian-armel update
            ;;
        debian-armhf)
            kodev-build debian-armhf
            make TARGET=debian-armhf update
            ;;
        debian-arm64)
            kodev-build debian-arm64
            make TARGET=debian-arm64 update
            ;;
        macos)
            is_mac
            kodev-build macos
            make TARGET=macos update
            ;;
        *)
            echo "Unsupported target for release: $1."
            echo "${RELEASE_HELP_MSG}"
            exit 1
            ;;
    esac
}

function kodev-wbuilder() {
    kodev-build
    echo "[*] Running wbuilder.lua..."
    pushd "${EMU_DIR}" && {
        EMULATE_READER_W=540 EMULATE_READER_H=720 ./luajit ./tools/wbuilder.lua
    } && popd || exit
}

function kodev-run() {
    RUN_HELP_MSG="
usage: run <OPTIONS> <TARGET>

OPTIONS:

    -h X, --screen-height=X  set height of the emulator screen (default: 720)
    -w X, --screen-width=X   set width of the emulator screen (default: 540)
    -d X, --screen-dpi=X     set DPI of the emulator screen (default: 160)
    -b, --no-build           run reader without rebuilding
    -n, --no-debug           no debugging symbols (requires rebuilding)
    -t, --disable-touch      use this if you want to simulate keyboard only devices
    -s FOO --simulate=FOO    simulate dimension and other specs for a given device model
                             supported model: hidpi, kobo-forma, kobo-aura-one, kobo-clara, kindle-paperwhite, kobo-h2o, legacy-paperwhite, kindle
    -g X, --gdb=X            run with debugger (default: nemiver)
    -p, --graph              graph memory use (requires gnuplot)
    -v X, --valgrind=X       run with valgrind (default: \"valgrind --tool=memcheck --trace-children=yes --leak-check=full --track-origins=yes --show-reachable=yes\")
    -c, --callgrind          run with valgrind's callgrind (valgrind --tool=callgrind --trace-children=yes)
    -S, --no-catchsegv       prevents wrapping by catchsegv

TARGET:

    android         install and run KOReader on an Android device connected through ADB
    "

    # NOTE: Speaking of Valgrind, if your libdrm/mesa isn't built w/ valgrind markings, there's a Valgrind suppression file for AMD cards in the tools folder.
    #       Just append --suppressions=${PWD/tools/valgrind_amd.supp to your valgrind command.

    # Defaults
    screen_width=540
    screen_height=720
    export KODEBUG=1

    declare opt
    declare -r E_OPTERR=85
    declare -r short_opts="tbng::pv::cw:h:d:s:SH"
    declare -r long_opts="disable-touch,no-build,gdb::,graph,valgrind::,callgrind,screen-width:,screen-height:,screen-dpi:,simulate:,no-catchsegv,help"

    if ! opt=$(getopt -o "${short_opts}" --long "${long_opts}" --name "kodev" -- "${@}"); then
        echo "${RUN_HELP_MSG}"
        exit ${E_OPTERR}
    fi

    eval set -- "${opt}"

    while true; do
        PARAM="${1}"
        # Support using an = assignment with short options (e.g., -f=blah instead of -f blah).
        VALUE="${2/#=/}"
        case "${PARAM}" in
            -t | --disable-touch)
                export DISABLE_TOUCH=1
                ;;
            -b | --no-build)
                no_build=1
                ;;
            -n | --no-debug)
                export KODEBUG=
                ;;
            -g | --gdb)
                if [ -n "${VALUE}" ]; then
                    gdb="${VALUE}"
                else
                    # Try to use friendly defaults for gdb
                    if command -v nemiver >/dev/null; then
                        # Nemiver is a nice GUI
                        gdb="nemiver"
                    elif command -v ddd >/dev/null; then
                        # DDD is a slightly less nice GUI
                        gdb="ddd"
                    elif command -v cgdb >/dev/null; then
                        # cgdb is a nice curses-based gdb front
                        gdb="cgdb"
                    elif command -v gdb >/dev/null; then
                        # gdb -tui is a slightly less nice terminal user interface
                        gdb="gdb -tui"
                    else
                        echo "Couldn't find gdb."
                        exit 1
                    fi
                fi
                shift
                ;;
            -p | --graph)
                graph_memory=1
                ;;
            -v | --valgrind)
                if [ -n "${VALUE}" ]; then
                    valgrind="${VALUE}"
                else
                    # Try to use sensible defaults for valgrind
                    if command -v valgrind >/dev/null; then
                        valgrind="valgrind --tool=memcheck --trace-children=yes --leak-check=full --track-origins=yes --show-reachable=yes"
                    else
                        echo "Couldn't find valgrind."
                        exit 1
                    fi
                fi
                shift
                ;;
            -c | --callgrind)
                # Try to use sensible defaults for valgrind
                if command -v valgrind >/dev/null; then
                    valgrind="valgrind --tool=callgrind --trace-children=yes"
                else
                    echo "Couldn't find valgrind."
                    exit 1
                fi
                ;;
            -w | --screen-width)
                screen_width=${VALUE}
                shift
                ;;
            -h | --screen-height)
                # simple numeric check due to overlap between -h for help and height
                if [ -n "${VALUE##*[!0-9]*}" ]; then
                    screen_height=${VALUE}
                else
                    echo "ERROR: Invalid value: \"${VALUE}\""
                    echo "${RUN_HELP_MSG}"
                    exit 1
                fi
                shift
                ;;
            -d | --screen-dpi)
                screen_dpi=${VALUE}
                shift
                ;;
            -s | --simulate)
                device_model="${VALUE}"
                case "${device_model}" in
                    kindle)
                        screen_width=600
                        screen_height=800
                        screen_dpi=167
                        ;;
                    legacy-paperwhite)
                        screen_width=758
                        screen_height=1024
                        screen_dpi=212
                        ;;
                    kobo-forma)
                        screen_width=1440
                        screen_height=1920
                        screen_dpi=300
                        ;;
                    kobo-aura-one)
                        screen_width=1404
                        screen_height=1872
                        screen_dpi=300
                        ;;
                    kobo-clara | kindle-paperwhite)
                        screen_width=1072
                        screen_height=1448
                        screen_dpi=300
                        ;;
                    kobo-h2o)
                        screen_width=1080
                        screen_height=1429
                        screen_dpi=265
                        ;;
                    hidpi)
                        screen_width=1500
                        screen_height=2000
                        screen_dpi=600
                        ;;
                    *)
                        echo "ERROR: spec unknown for ${device_model}."
                        ;;
                esac
                shift
                ;;
            -S | --no-catchsegv)
                no_catchsegv=1
                ;;
            -H | --help)
                echo "${RUN_HELP_MSG}"
                exit 0
                ;;
            --)
                break
                ;;
            *)
                echo "ERROR: unknown option \"${PARAM}\""
                echo "${RUN_HELP_MSG}"
                exit 8
                ;;
        esac
        shift
    done
    shift

    case "${1}" in
        android)
            command -v adb >/dev/null && {
                if [ -z "${no_build}" ]; then
                    echo "[*] Building KOReader for Android…"
                    kodev-release --ignore-translation android
                    assert_ret_zero $?
                fi
                setup_env
                # clear logcat to get rid of useless cruft
                adb logcat -c
                # uninstall existing package to make sure *everything* is gone from memory
                # no assert_ret_zero; uninstall is allowed to fail if there's nothing to uninstall
                adb uninstall "org.koreader.launcher"
                adb install "koreader-android-${ANDROID_ARCH}${KODEBUG_SUFFIX}-${VERSION}.apk"
                assert_ret_zero $?
                # there's no adb run so we do this…
                adb shell monkey -p org.koreader.launcher -c android.intent.category.LAUNCHER 1
                assert_ret_zero $?
                adb logcat KOReader:V luajit-launcher:V "*:E"
            } || echo "Failed to find adb in PATH to interact with Android device."
            ;;
        *)
            if [ -z "${no_build}" ]; then
                echo "[*] Building KOReader…"
                if [ -z "${KODEBUG}" ]; then
                    kodev-build --no-debug
                else
                    kodev-build
                fi
            else
                setup_env
            fi

            if [ ! -d "${EMU_DIR}" ]; then
                echo "Failed to find emulator directory! Please try build command first."
                exit 1
            fi

            if [ -n "${graph_memory}" ]; then
                gnuplot_wrapper "--parent $$" "reader.lua"
            fi

            KOREADER_ARGS="-d"
            KOREADER_COMMAND="env LD_LIBRARY_PATH=${KO_LD_LIBRARY_PATH} ./reader.lua ${KOREADER_ARGS}"

            # run with catchsegv by default when it is available (unless no-catchsegv is enabled, c.f., #7036)
            # see https://github.com/koreader/koreader/issues/2878#issuecomment-326796777
            if [ -z "${no_catchsegv}" ]; then
                if command -v catchsegv >/dev/null; then
                    KOREADER_COMMAND="$(command -v catchsegv) ${KOREADER_COMMAND}"
                fi
            fi

            if [ -n "${valgrind}" ]; then
                KOREADER_COMMAND="${valgrind} env LD_LIBRARY_PATH=${KO_LD_LIBRARY_PATH} ./luajit reader.lua ${KOREADER_ARGS}"
            fi

            echo "[*] Running KOReader with arguments: $* ..."
            pushd "${EMU_DIR}" && {
                if [ $# -ge 1 ]; then
                    args="$*"
                    [[ "${args}" != /* ]] && args="${CURDIR}/${args}"
                fi

                if [ -n "${gdb}" ]; then
                    # We don't want to stack valgrind/catchsegv on top of GDB ;).
                    if [[ "${gdb}" == gdb* ]]; then
                        # The standard CLI needs a little hand holding to properly pass arguments to the process it'll monitor
                        KOREADER_COMMAND="${gdb} --args env LD_LIBRARY_PATH=${KO_LD_LIBRARY_PATH} ./luajit reader.lua ${KOREADER_ARGS} ${args}"
                    else
                        KOREADER_COMMAND="${gdb} env LD_LIBRARY_PATH=${KO_LD_LIBRARY_PATH} ./luajit reader.lua ${KOREADER_ARGS} ${args}"
                    fi
                else
                    KOREADER_COMMAND="${KOREADER_COMMAND} ${args}"
                fi

                RETURN_VALUE=85
                while [ "${RETURN_VALUE}" -eq 85 ]; do
                    # shellcheck disable=SC2086
                    env EMULATE_READER_W="${screen_width}" EMULATE_READER_H="${screen_height}" EMULATE_READER_DPI="${screen_dpi}" \
                        ${KOREADER_COMMAND}
                    RETURN_VALUE=$?
                done
            } && popd || exit

            if [ -n "${graph_memory}" ]; then
                capture_ctrl_c
            fi

            exit ${RETURN_VALUE}
            ;;
    esac
}

function kodev-test() {
    TEST_HELP_MSG="
usage: test <OPTIONS> [front|base] <TEST_NAME>

    TEST_NAME is optional. If no TEST_NAME is given, all tests will be run.

OPTIONS:

    -p, --graph         graph memory use (requires gnuplot)
    -n, --no-debug      no debugging symbols (default for target devices)
    -t, --tags=TAGS     only run tests with given tags
    "

    declare opt
    declare -r E_OPTERR=85
    declare -r short_opts="pt:nh"
    declare -r long_opts="graph,tags:,no-debug,help"

    if ! opt=$(getopt -o "${short_opts}" --long "${long_opts}" --name "kodev" -- "${@}"); then
        echo "${TEST_HELP_MSG}"
        exit ${E_OPTERR}
    fi

    eval set -- "${opt}"

    while true; do
        PARAM="${1}"
        # Support using an = assignment with short options (e.g., -f=blah instead of -f blah).
        VALUE="${2/#=/}"
        case "${PARAM}" in
            -p | --graph)
                graph_memory=1
                ;;
            -n | --no-debug)
                export KODEBUG=
                KODEBUG_NO_DEFAULT=1
                ;;
            -t | --tags)
                opts="--tags=${VALUE}"
                shift
                ;;
            -h | --help)
                echo "${TEST_HELP_MSG}"
                exit 0
                ;;
            --)
                break
                ;;
            *)
                echo "ERROR: unknown option \"${PARAM}\""
                echo "${TEST_HELP_MSG}"
                exit 8
                ;;
        esac
        shift
    done
    shift

    [ $# -lt 1 ] && {
        echo "${TEST_HELP_MSG}"
        exit 1
    }
    [[ "${1}" != "front" && "${1}" != "base" ]] && {
        echo "Invalid test suite: $1!"
        echo "${TEST_HELP_MSG}"
        exit 1
    }

    set -e
    check_submodules && kodev-build
    setup_env

    make "${EMU_DIR}/.busted"
    pushd "${EMU_DIR}" && {
        test_path_basedir="./spec/$1/unit"
        rm -rf "${test_path_basedir}"/data/*.sdr

        test_path="${test_path_basedir}"
        if [ -n "${2}" ]; then
            test_path="${test_path_basedir}/$2"
        fi

        echo "Running tests in" "${test_path}"
        env LD_LIBRARY_PATH="${KO_LD_LIBRARY_PATH}" \
            busted --lua="./luajit" "${opts}" \
            --output=gtest \
            --lpath="${test_path_basedir}/?.lua" \
            --exclude-tags=notest "${test_path}"
    } && popd || exit
}

function kodev-cov() {
    COV_HELP_MSG="
usage: cov <OPTIONS>

OPTIONS:

    -f, --full              show full coverage report (down to each line)
    -s, --show-previous     show coverage stats from previous run
    -n, --no-debug          no debugging symbols (default for target devices)
    "

    # Defaults
    show_full=0
    show_previous=0

    declare opt
    declare -r E_OPTERR=85
    declare -r short_opts="fsn"
    declare -r long_opts="full,show-previous,no-debug"

    if ! opt=$(getopt -o "${short_opts}" --long "${long_opts}" --name "kodev" -- "${@}"); then
        echo "${COV_HELP_MSG}"
        exit ${E_OPTERR}
    fi

    eval set -- "${opt}"

    while true; do
        PARAM="${1}"
        # Support using an = assignment with short options (e.g., -f=blah instead of -f blah).
        VALUE="${2/#=/}"
        case "${PARAM}" in
            -f | --full)
                show_full=1
                ;;
            -s | --show-previous)
                show_previous=1
                ;;
            -n | --no-debug)
                export KODEBUG=
                KODEBUG_NO_DEFAULT=1
                ;;
            -h | --help)
                echo "${COV_HELP_MSG}"
                exit 0
                ;;
            --)
                break
                ;;
            *)
                echo "ERROR: unknown option \"${PARAM}\""
                echo "${COV_HELP_MSG}"
                exit 8
                ;;
        esac
        shift
    done
    shift

    set -e
    check_submodules && kodev-build
    setup_env
    make "${EMU_DIR}/.busted"
    pushd "${EMU_DIR}" && {
        target=front
        test_path="./spec/${target}/unit"
        if [ "${show_previous}" -eq 0 ]; then
            echo "Running tests in" ${test_path}
            env LD_LIBRARY_PATH="${KO_LD_LIBRARY_PATH}" \
                busted --lua="./luajit" \
                --sort-files \
                -o "./spec/${target}/unit/verbose_print" \
                --coverage \
                --exclude-tags=nocov "${test_path}" || {
                echo "Failed to run tests!" && exit 1
            }
        fi
        if [ "${show_full}" -eq 1 ]; then
            cat luacov.report.out
        else
            LUACOV_REPORT_SUMMARY=$(grep -nm1 -e '^Summary$' luacov.report.out | cut -d: -f1)
            tail -n \
                +$((LUACOV_REPORT_SUMMARY - 1)) \
                luacov.report.out
        fi
    } && popd || exit
}

function kodev-log() {
    LOG_HELP_MSG="
usage: log <OPTIONS> <TARGET>

OPTIONS:

    -d, --debug               more verbose logs (e.g., debug builds)

TARGET:

    android
"
    [ $# -lt 1 ] && {
        echo "${LOG_HELP_MSG}"
        exit 1
    }

    # Defaults
    ignore_translation=0

    declare opt
    declare -r E_OPTERR=85
    declare -r short_opts="dh"
    declare -r long_opts="debug, help"

    if ! opt=$(getopt -o "${short_opts}" --long "${long_opts}" --name "kodev" -- "${@}"); then
        echo "${LOG_HELP_MSG}"
        exit ${E_OPTERR}
    fi

    eval set -- "${opt}"

    while true; do
        PARAM="${1}"
        # Support using an = assignment with short options (e.g., -f=blah instead of -f blah).
        VALUE="${2/#=/}"
        case "${PARAM}" in
            -d | --debug)
                export KODEBUG=1
                ;;
            -h | --help)
                echo "${LOG_HELP_MSG}"
                exit 0
                ;;
            --)
                break
                ;;
            *)
                echo "ERROR: unknown option \"${PARAM}\""
                echo "${RELEASE_HELP_MSG}"
                exit 1
                ;;
        esac
        shift
    done
    shift

    case "${1}" in
        android)
            if command -v pidcat >/dev/null; then
                if [ -n "${KODEBUG}" ]; then
                    pidcat "org.koreader.launcher"
                else
                    pidcat org.koreader.launcher --min-level=I
                fi
            else
                if [ -n "${KODEBUG}" ]; then
                    adb logcat 'KOReader:V ApkUpdater:V Assets:V Device:V dlopen:V EPD:V EventReceiver:V Lights:V LuaJIT:V MainActivity:V NativeGlue:V NativeThread:V Timeout:V ActivityManager:V AndroidRuntime:V DEBUG:* *:F'
                else
                    adb logcat 'KOReader:I MainActivity:V NativeGlue:V NativeThread:V ActivityManager:W AndroidRuntime:E DEBUG:* *:F'
                fi
            fi
            ;;
        *)
            echo "Unsupported target: $1."
            echo "${LOG_HELP_MSG}"
            exit 1
            ;;
    esac
}

HELP_MSG="
usage: $0 COMMAND <ARGS>

Supported commands:

    activate            Bootstrap shell environment for kodev
    build               Build KOReader
    clean               Clean KOReader build
    fetch-thirdparty    Fetch thirdparty dependencies for build
    log                 Tail log stream for a running KOReader app
    release             Build KOReader release package
    run                 Run KOReader
    test                Run busted tests
    check               Run luacheck static-analysis
    cov                 Run busted tests for coverage
    wbuilder            Run wbuilder.lua script (useful for building new UI widget)
    prompt              Run a LuaJIT shell within KOReader's environment
"
[ $# -lt 1 ] && {
    echo "Missing command."
    echo "${HELP_MSG}"
    exit 1
}

case "${1}" in
    activate)
        echo "adding ${CURDIR} to \$PATH..."
        export PATH="${PATH}:${CURDIR}"
        eval "$(luarocks path --bin)"
        exec "${SHELL}"
        ;;
    fetch-thirdparty)
        kodev-fetch-thirdparty
        ;;
    clean)
        shift 1
        kodev-clean "$@"
        ;;
    build)
        shift 1
        kodev-build "$@"
        ;;
    release)
        shift 1
        kodev-release "$@"
        ;;
    wbuilder)
        kodev-wbuilder
        ;;
    run)
        shift 1
        kodev-run "$@"
        ;;
    test)
        shift 1
        kodev-test "$@"
        ;;
    check)
        luacheck -q {reader,setupkoenv,datastorage}.lua frontend plugins spec
        ;;
    cov)
        shift 1
        kodev-cov "$@"
        ;;
    prompt)
        kodev-build
        pushd "${EMU_DIR}" && {
            env LD_LIBRARY_PATH="${KO_LD_LIBRARY_PATH}" \
                ./luajit -i setupkoenv.lua
        } && popd || exit
        ;;
    log)
        shift 1
        kodev-log "$@"
        ;;
    -h | --help)
        echo "${HELP_MSG}"
        exit 0
        ;;
    *)
        echo "Unknown command: $1."
        echo "${HELP_MSG}"
        exit 8
        ;;
esac
